use std::mem;

use godot::prelude::*;

#[derive(GodotClass)]
#[class(init, base=Object)]
pub struct ZMath {}

#[godot_api]
impl ZMath {
    #[func]
    pub fn add(lhs: Variant, rhs: Variant) -> Variant {
        lhs.evaluate(&rhs, VariantOperator::ADD).unwrap_or_default()
    }

    #[func]
    pub fn sub(lhs: Variant, rhs: Variant) -> Variant {
        lhs.evaluate(&rhs, VariantOperator::SUBTRACT)
            .unwrap_or_default()
    }

    #[func]
    pub fn mul(lhs: Variant, rhs: Variant) -> Variant {
        lhs.evaluate(&rhs, VariantOperator::MULTIPLY)
            .unwrap_or_default()
    }

    #[func]
    pub fn div(lhs: Variant, rhs: Variant) -> Variant {
        lhs.evaluate(&rhs, VariantOperator::DIVIDE)
            .unwrap_or_default()
    }

    #[func]
    pub fn or(lhs: Variant, rhs: Variant) -> Variant {
        lhs.evaluate(&rhs, VariantOperator::OR).unwrap_or_default()
    }

    #[func]
    pub fn and(lhs: Variant, rhs: Variant) -> Variant {
        lhs.evaluate(&rhs, VariantOperator::AND).unwrap_or_default()
    }

    #[func]
    pub fn xor(lhs: Variant, rhs: Variant) -> Variant {
        lhs.evaluate(&rhs, VariantOperator::XOR).unwrap_or_default()
    }

    #[func]
    pub fn floor_pixel_perfect(value: i32) -> i32 {
        2i32.pow((value as f32).log2().floor() as u32)
    }

    #[func]
    pub fn ceil_pixel_perfect(value: i32) -> i32 {
        2i32.pow((value as f32).log2().ceil() as u32)
    }

    #[func]
    pub fn floor_vec2(v: Vector2) -> Vector2i {
        Vector2i::new(v.x.floor() as i32, v.y.floor() as i32)
    }

    #[func]
    pub fn floor_vec3(v: Vector3) -> Vector3i {
        Vector3i::new(v.x.floor() as i32, v.y.floor() as i32, v.z.floor() as i32)
    }

    #[func]
    pub fn ceil_vec2(v: Vector2) -> Vector2i {
        Vector2i::new(v.x.ceil() as i32, v.y.ceil() as i32)
    }

    #[func]
    pub fn ceil_vec3(v: Vector3) -> Vector3i {
        Vector3i::new(v.x.ceil() as i32, v.y.ceil() as i32, v.z.ceil() as i32)
    }

    #[func]
    pub fn splat_vec2(x: f32) -> Vector2 {
        Vector2::splat(x)
    }

    #[func]
    pub fn splat_vec3(x: f32) -> Vector3 {
        Vector3::splat(x)
    }

    #[func]
    pub fn square(position: Vector2, radius: f32) -> Rect2 {
        let x = position.x - radius;
        let y = position.y - radius;
        let size = 2.0 * radius;

        Rect2::from_components(x, y, size, size)
    }

    #[func]
    pub fn minmax(a: i64, b: i64) -> Array<i64> {
        let (min, max) = Self::minmax_raw(a, b);
        array![min, max]
    }

    #[func]
    pub fn minmaxv(mut v: Vector2) -> Vector2 {
        if v.x > v.y {
            mem::swap(&mut v.x, &mut v.y);
        }

        v
    }

    #[func]
    pub fn minmaxvi(mut v: Vector2i) -> Vector2i {
        if v.x > v.y {
            mem::swap(&mut v.x, &mut v.y);
        }

        v
    }

    #[func]
    pub fn nearest(
        origin: Vector2i,
        relatives: VariantArray,
        position: Vector2,
        map_func: Callable,
        filter: Callable,
    ) -> Variant {
        relatives
            .iter_shared()
            .map(|x| origin + x.to::<Vector2i>())
            .map(|x| map_func.call(&[x.to_variant()]).to::<Vector2>())
            .filter(|x| filter.call(&[x.to_variant()]).booleanize())
            .min_by(|a, b| {
                a.distance_squared_to(position)
                    .total_cmp(&b.distance_squared_to(position))
            })
            .map(|x| x.cast_int().to_variant())
            .unwrap_or_default()
    }

    #[func]
    pub fn nearest_vec2i(
        o: Vector2i,
        offsets: VariantArray,
        to: Vector2i,
        filter: Callable,
    ) -> Variant {
        offsets
            .iter_shared()
            .map(|x| o + x.to::<Vector2i>())
            .filter(|x| filter.call(&[x.to_variant()]).booleanize())
            .min_by(|a, b| a.distance_squared_to(to).cmp(&b.distance_squared_to(to)))
            .map(|x| x.to_variant())
            .unwrap_or_default()
    }
}

impl ZMath {
    pub fn minmax_raw(a: i64, b: i64) -> (i64, i64) {
        if a > b { (b, a) } else { (a, b) }
    }
}
